package trip

import (
	"context"
	rentalpb "coolcar/proto/rental_out/v1"
	"coolcar/srvs/rental_srv/dao/trip"
	"math/rand"
	"time"

	"google.golang.org/grpc/codes"
	"google.golang.org/grpc/status"

	"go.uber.org/zap"
)

type Service struct {
	Mongo          *trip.Mongo
	Logger         *zap.Logger
	ProfileManager ProfileManager
	CarManager     CarManager
	POIManager     POIManager
	rentalpb.UnimplementedTripServiceServer
}

type ProfileManager interface {
	Verify(c context.Context, account string) (identityID string, err error)
}

type CarManager interface {
	Verify(c context.Context, carID string, location *rentalpb.Location) error
	Unlock(c context.Context, cadID string) error
}

type POIManager interface {
	Resolve(c context.Context, location *rentalpb.Location) (poiName string, err error)
}

func (s Service) CreateTrip(c context.Context, request *rentalpb.CreateTripRequest) (*rentalpb.TripEntity, error) {
	if request.CarId == "" || request.Start == nil {
		return nil, status.Error(codes.InvalidArgument, "参数❌错误")
	}

	// 1.验证驾驶者身份
	// identityID用于做交易快照(确定此时开车的人是谁, 以免开锁后中途重新审查身份)
	identityID, err := s.ProfileManager.Verify(c, "account1")
	if err != nil {
		return nil, status.Error(codes.Unauthenticated, "身份验证失败")
	}

	// 2.检查车辆状态
	err = s.CarManager.Verify(c, request.CarId, request.Start)
	if err != nil {
		return nil, status.Error(codes.FailedPrecondition, "车辆不可用")
	}

	// 3.查询地标
	poi, err := s.POIManager.Resolve(c, request.Start)
	if err != nil {
		s.Logger.Info("can not resolve poi", zap.Stringer("location", request.Start), zap.Error(err))
	}

	// 4.插入数据库
	nowStatus := &rentalpb.LocationStatus{
		Location: request.Start,
		PoiName:  poi,
	}
	tr, err := s.Mongo.CreateTrip(c, &rentalpb.Trip{
		AccountId:  "account1",
		CarId:      request.CarId,
		IdentityId: identityID,
		Start:      nowStatus,
		Current:    nowStatus,
		Status:     rentalpb.TripStatus_IN_PROGRESS,
	})
	if err != nil {
		s.Logger.Info("can not create trip", zap.Error(err))
		return nil, status.Error(codes.AlreadyExists, "创建行程失败")
	}

	// 5.车辆开锁
	go func() {
		err := s.CarManager.Unlock(c, request.CarId)
		if err != nil {
			s.Logger.Info("can not Unlock car", zap.Error(err))
		}
	}()

	return &rentalpb.TripEntity{
		Id:   tr.ID.Hex(),
		Trip: tr.Trip,
	}, nil
}

func (s Service) GetTrip(c context.Context, request *rentalpb.GetTripRequest) (*rentalpb.Trip, error) {
	tr, err := s.Mongo.GetTrip(c, request.Id, "account1")
	if err != nil {
		return nil, status.Error(codes.NotFound, "")
	}

	return tr.Trip, nil
}

func (s Service) GetTrips(c context.Context, request *rentalpb.GetTripsRequest) (*rentalpb.GetTripsResponse, error) {
	trips, err := s.Mongo.GetTrips(c, "accountid", request.Status)
	if err != nil {
		s.Logger.Error("GetTrips failed", zap.Error(err))
		return nil, status.Error(codes.Internal, "")
	}
	rsp := rentalpb.GetTripsResponse{}
	for _, trip := range trips {
		rsp.Trips = append(rsp.Trips, &rentalpb.TripEntity{
			Id:   trip.ID.Hex(),
			Trip: trip.Trip,
		})
	}
	return &rsp, nil
}

func (s Service) UpdateTrip(c context.Context, request *rentalpb.UpdateTripRequest) (*rentalpb.Trip, error) {
	// 查询
	tr, err := s.Mongo.GetTrip(c, request.Id, "account1")
	if err != nil {
		return nil, status.Error(codes.NotFound, "")
	}

	if tr.Trip.Current == nil {
		s.Logger.Error("trip without current set", zap.String("id", request.Id))
		return nil, status.Error(codes.Internal, "")
	}

	cur := tr.Trip.Current.Location
	if request.Current != nil {
		cur = request.Current
	}

	tr.Trip.Current = s.calcCurrentStatus(c, tr.Trip.Current, cur)

	if request.EndTrip {
		tr.Trip.End = tr.Trip.Current
		tr.Trip.Status = rentalpb.TripStatus_FINISHED
	}

	// 更新
	err = s.Mongo.UpdateTrip(c, request.Id, "account1", tr.UpdatedAt, tr.Trip)
	if err != nil {
		return nil, status.Error(codes.Aborted, "")
	}
	return tr.Trip, nil
}

const (
	centsPerSec = 0.7
	kmPerSec    = 0.02
)

func (s Service) calcCurrentStatus(c context.Context, last *rentalpb.LocationStatus, cur *rentalpb.Location) *rentalpb.LocationStatus {
	elapseSed := float64(time.Now().Unix() - last.TimestampSec)
	// 3.查询地标
	poi, err := s.POIManager.Resolve(c, cur)
	if err != nil {
		s.Logger.Info("can not resolve poi", zap.Stringer("location", cur), zap.Error(err))
	}

	return &rentalpb.LocationStatus{
		Location:     cur,
		FeeCent:      last.FeeCent + int32(centsPerSec*elapseSed*2*rand.Float64()),
		KmDriven:     last.KmDriven + kmPerSec*elapseSed*2*rand.Float64(),
		TimestampSec: time.Now().Unix(),
		PoiName:      poi,
	}
}
